/**************************************************************************
 *
 * Copyright (C) 2007 University of California, Los Angeles.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *************************************************************************/

package edu.ucla.cs.typecast.rmi;

import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.*;
import java.util.Hashtable;
import java.util.Random;

import edu.ucla.cs.typecast.net.EndPoint;
import edu.ucla.cs.typecast.net.TransportContext;
import edu.ucla.cs.typecast.net.TransportSender;
import edu.ucla.cs.typecast.net.TransportSenderFactory;
import edu.ucla.cs.typecast.net.TypeCastConstants;
import edu.ucla.cs.typecast.net.TypeCastDispatcher;
import edu.ucla.cs.typecast.rmi.runtime.RMIReturnValueProcessor;
import edu.ucla.cs.typecast.rmi.runtime.RMIUtil;
import edu.ucla.cs.typecast.scope.HostScope;
import edu.ucla.cs.typecast.scope.NetScope;
import edu.ucla.cs.typecast.scope.Scope;

public class TypeCastClient implements TypeCastConstants, RMIReturnValueProcessor, TypeCastDispatcher {
	protected static final int DEFAULT_TIMEOUT = 10000;

	private TransportSender transportSender; 
	private InvocationMarshaller invocationMarshaller = new InvocationMarshaller(); 
	private EndPoint endPoint; 
	private Hashtable<Long, ReturnSet> resultCorrelationTable = new Hashtable<Long, ReturnSet>(); 
	
	protected void finalize() { 
		if (transportSender != null) { 
			try { 
				transportSender.close(); 
			} catch(Throwable t) {}
		}
	}
	
	public TypeCastClient() throws IOException, UnknownHostException {
		this(InetAddress.getByName(DEFAULT_RMI_ADDRESS), DEFAULT_RMI_PORT);
	}

	public TypeCastClient(InetAddress address, int port) throws IOException {
		this(new EndPoint(address, port)); 
	}

	public TypeCastClient(String address, int port) throws IOException, UnknownHostException {
		this(InetAddress.getByName(address), port);
	}

	public TypeCastClient(EndPoint endPoint) throws IOException, UnknownHostException { 
		this.endPoint = endPoint; 
		this.transportSender = TransportSenderFactory.createTransportSender(endPoint); 
	}
	
	public void setScope(Scope scope) throws IOException { 
		if (scope instanceof NetScope) { 
			if (! (scope instanceof HostScope)) { 
				try { 
					this.transportSender.setNetScope((NetScope)scope); 
				} catch(Throwable t) { 
					throw new RuntimeException(t); 
				}
			} else { 
				if (transportSender != null) { 
					try { 
						transportSender.close(); 
					} catch(Throwable t) {}
				}
				HostScope hostScope = (HostScope)scope;
				EndPoint newEndPoint = new EndPoint(hostScope.getHost(), endPoint.getPort()); 
				transportSender = TransportSenderFactory.createTransportSender(newEndPoint); 
			}
		}
	}
	public void setInvocationMarshaller(InvocationMarshaller marshaller) { 
		this.invocationMarshaller = marshaller; 
	}
	
	public InetAddress getAddress() {
		return endPoint.getAddress();
	}

	public int getPort() {
		return endPoint.getPort();
	}

	public Object getTypeCastInvocationHandler(Class type) {
		InvocationHandler handler = new TypeCastClientHandler(this, transportSender);
		Class[] types = { type };

		Object proxy = Proxy.newProxyInstance((TypeCastClient.class).getClassLoader(), types, handler);
		return proxy;
	}

	public <T> T getInvocationHandler(Class<T> type) { 
		TypeCastClientHandler handler = new TypeCastClientHandler(this, transportSender);
		handler.setInvocationMarshaller(invocationMarshaller); 
		Object proxy = Proxy.newProxyInstance((TypeCastClient.class).getClassLoader(), new Class[] { type }, handler);
		return (T) proxy;		
	}
		
	public Object getTypeCastInvocationHandler(Class[] types) {
		TypeCastClientHandler handler = new TypeCastClientHandler(this, transportSender);
		handler.setInvocationMarshaller(invocationMarshaller); 
		Object proxy = Proxy.newProxyInstance((TypeCastClient.class).getClassLoader(), types, handler);
		return proxy;
	}
	
	public TransportSender getTransportSender() { 
		return this.transportSender; 
	}
	
	public void close() throws IOException { 
		try { 
			transportSender.close(); 
		} finally { 
			transportSender = null; 
		}
		
	}
	
	synchronized ReturnSet createNewFutureForReturnSet() { 
		Random random = new Random(); 
		Long corID = random.nextLong(); 
		while (this.resultCorrelationTable.contains(corID))  { 
			corID = random.nextLong(); 
		}
		ReturnSet future = new ReturnSet(this, corID); 
		this.resultCorrelationTable.put(corID, future);
		return future; 		
	}
	
	synchronized void  removeFuture(ReturnSet future) { 
		this.resultCorrelationTable.remove(future.getCorrelationID()); 
		if (resultCorrelationTable.size() == 0) { 
			try  { 
				this.transportSender.getLocalEndPointListener().deactivate(null); 
			} catch(Throwable t) {}
		}
	}
		
	public void process(Long correlationID, Object result) { 
		ReturnSet returnSet = resultCorrelationTable.get(correlationID); 
		if (returnSet != null) { 
			returnSet.add(result); 
		}
	}

	public void dispatch(TransportContext transportContext,
			InputStream inputStream) throws IOException {
		InvocationUnmarshaller unmarshaller = new InvocationUnmarshaller(); 
		MethodInvocation invocation = unmarshaller.unmarshall(inputStream); 
		Method method = invocation.getMethod(); 
		if (method != null && method.equals(RMIUtil.getMethodForProcessReturnValue())) { 
			try { 
				method.invoke(this, invocation.getArguments()); 
			} catch(Throwable t) { 
				t.printStackTrace(); 
			}
		} else { 
			System.out.println("Error: invalid method invocation received " + method); 
		}
	}
	
}
